package com.quantum.logging
{

	import flash.events.Event;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;

	import mx.logging.ILogger;
	import mx.logging.ILoggingTarget;
	import mx.logging.Log;
	import mx.logging.LogEvent;
	import mx.logging.LogEventLevel;

	public class LogFactory
	{
		[ArrayElementType("mx.logging.LogEvent")]
		/**
		 * Contains the log events dispatched while the log
		 * config files are downloaded.
		 * When a config is done downloading and its trace
		 * targets are created, the pending log messages are re-logged.
		 */
		private static var _pendingEvents:Array=[];

		/**
		 * The number of pending config file downloads.
		 */
		private static var _pendingLoadCount:int;

		/**
		 * Custom level strings by level numeric values.
		 */
		private static var _customLevelStringByLevel:Dictionary /*int, String*/=new Dictionary();

		/**
		 * The next custom level value available to use.
		 */
		private static var _nextCustomLevel:int=LogEventLevel.FATAL + 1;


		/**
		 * Returns a logger object for the given category.
		 *
		 * Note that if the class calling this method could be
		 * instantiated multiple times, then it is recommended to
		 * assign the result logger to a static variable because the
		 * Flex <code>Log</code> class keeps a strong-reference cache
		 * of all logger objects.
		 *
		 * @param category
		 *    The <code>ILogger</code> category specified as Class,
		 *    Object or String.
		 *    Passing a Class or Object is a shortcut for passing
		 *    the qualified class name.
		 */
		public static function getLogger(category:*):ILogger
		{
			var categoryStr:String=category as String;
			if (categoryStr == null)
			{
				categoryStr=getQualifiedClassName(category);
				// : is an invalid character in category names.
				categoryStr=categoryStr.replace("::", ".");
			}

			var logger:ILogger=Log.getLogger(categoryStr);

			// If there is pending config downloading in progress
			// then listen to the log events so that message are
			// re-logged when the config logging targets are created.
			if (_pendingLoadCount != 0)
			{
				logger.addEventListener(LogEvent.LOG, onLog);
			}
			return logger;
		}

		/**
		 * Adds the logging targets specified in the given xml.
		 * The configuration xml schema is described in the class
		 * documentation.
		 */
		public static function addConfiguration(configXml:XML):void
		{
//			var config:LogConfiguration=new LogConfiguration(configXml);
//			for each (var target:ILoggingTarget in config.targets)
//			{
//				Log.addTarget(target);
//			}
		}

		/**
		 * Loads a log configuration from the given uri and adds its
		 * logging targets.
		 * The configuration xml schema is described in the class
		 * documentation.
		 *
		 * @param uri
		 *    The uri of the log configuration file.
		 *
		 * @param callback
		 *    <code>function(error:Error, callContext:Object=null)</code>.
		 *
		 * @param callContext
		 *    The context object passed to the addConfigurationByUri
		 *     method.
		 */
		public static function addConfigurationByUri(uri:String, callback:Function, callContext:Object=null):void
		{
			var loader:ConfigurationLoader=new ConfigurationLoader();

			var pendingContext:Object={pendingCallback: callback, pendingCallContext: callContext};

			++_pendingLoadCount;
			loader.load(uri, onConfigLoaded, pendingContext);
		}

		/**
		 * Adds a custom logging level.
		 *
		 * @param levelString
		 *    The level string logged along with log messages.
		 *
		 * @return
		 *    The level numeric value that can be used in <code>ILogger.log</code> calls.
		 */
		public static function addCustomLevel(levelString:String):int
		{
			var level:int=getCustomLevel(levelString);
			if (level != -1)
			{
				return level;
			}
			level=_nextCustomLevel++;
			_customLevelStringByLevel[level]=levelString;
			return level;
		}

		/**
		 * Removes a custom logging level.
		 *
		 * @param levelString
		 *    The level string that was used in <code>addCustomLevel</code>.
		 */
		public static function removeCustomLevel(levelString:String):void
		{
			var level:int=getCustomLevel(levelString);
			if (level == -1)
			{
				return;
			}
			delete _customLevelStringByLevel[level];
		}

		/**
		 * Tests whether the given level is valid; i.e is a standard level or currently added
		 * custom level.
		 */
		public static function isValidLevel(level:int):Boolean
		{
			if (level <= LogEventLevel.FATAL)
			{
				return true;
			}
			return getCustomLevelString(level) != null;
		}

		/**
		 * Returns the custom level string given a level numeric value, or null if no custom
		 * level corresponds to the given value.
		 */
		public static function getCustomLevelString(level:int):String
		{
			var levelStr:String=_customLevelStringByLevel[level]
			return levelStr;
		}

		/**
		 * Returns the custom logging level corresponding to the given level string, or
		 * -1 if none.
		 */
		private static function getCustomLevel(levelStr:String):int
		{
			for (var levelObj:Object in _customLevelStringByLevel)
			{
				var level:int=int(levelObj);
				if (_customLevelStringByLevel[level] == levelStr)
				{
					return level;
				}
			}
			return -1;
		}

		/**
		 * The callback of the <code>ConfigurationLoader.load</code> function.
		 */
		private static function onConfigLoaded(configXml:XML, error:Error, callContext:Object):void
		{
			if (error == null)
			{
				addConfiguration(configXml);
			}
			if (--_pendingLoadCount == 0)
			{
				flushPendingEvents();
			}
			callContext.pendingCallback(error, callContext.pendingCallContext);
		}

		/**
		 * The event handler of the og events disptached while
		 * config files are being downloaded.
		 */
		private static function onLog(event:LogEvent):void
		{
			_pendingEvents.push(event);
		}

		/**
		 * Re-logs the messages corresponding to the pending
		 * events.
		 */
		private static function flushPendingEvents():void
		{
			for each (var event:LogEvent in _pendingEvents)
			{
				var logger:ILogger=ILogger(event.target);
				// This could be called multiple times if there are
				// multiple events for a logger, but that's ok.
				logger.removeEventListener(LogEvent.LOG, onLog);

				logger.log(event.level, event.message);
			}
			_pendingEvents=[];
		}
	}
}

////////////////////////////////////////////////////////////////////////
// Private classes
////////////////////////////////////////////////////////////////////////

import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.net.URLRequest;
import flash.net.URLLoader;

/**
 * Loads a logging configuration given its uri.
 */
class ConfigurationLoader
{
	private var _callback:Function;
	private var _callContext:Object;

	/**
	 * Loads a log configuration xml from the given uri.
	 *
	 * @param uri
	 *    The uri of the log configuration file.
	 *
	 * @param callback
	 *    <code>function(configXml:XML, error:Error,
	 *       callContext:Object=null)</code>.
	 *
	 * @param callContext
	 *    The context object passed to the load method.
	 */
	public function load(uri:String, callback:Function, callContext:Object=null):void
	{
		_callback=callback;
		_callContext=callContext;

		var loader:URLLoader=new URLLoader();
		loader.addEventListener(Event.COMPLETE, onLoadSuccess);
		loader.addEventListener(IOErrorEvent.IO_ERROR, onLoadError);
		loader.load(new URLRequest(uri));
	}

	private function onLoadSuccess(event:Event):void
	{
		var loader:URLLoader=URLLoader(event.target);
		var configXml:XML=new XML(loader.data);
		_callback(configXml, null, _callContext);
	}

	private function onLoadError(event:IOErrorEvent):void
	{
		_callback(null, new Error(event.text), _callContext);
	}
}